﻿

#region Using Statements

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Threading;
using System.ComponentModel;
using System.Reflection;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Design;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using Freeform;
using FGame = Freeform.Game;
using XGame = Microsoft.Xna.Framework.Game;
using FComponent = Freeform.Component;

#endregion


namespace Freeform
{
    /// <summary>
    /// Actors are used to graphically represent a body through a viewport.
    /// </summary>
    public class Actor3D_Model : Actor3D
    {

        #region Members

        /// <summary>
        /// Gets or sets the model used for this Actor.
        /// </summary>
        public Model Model { get; set; }

        public Vector3 OrientationOffset { get { return _OrientationOffset; } set { _OrientationOffset = value; ComputeOffset(); } }
        private Vector3 _OrientationOffset = Vector3.Zero;

        public Vector3 ScaleOffset { get { return _ScaleOffset; } set { _ScaleOffset = value; ComputeOffset(); } }
        private Vector3 _ScaleOffset = Vector3.Zero;

        public Vector3 PositionOffset { get { return _PositionOffset; } set { _PositionOffset = value; ComputeOffset(); } }
        private Vector3 _PositionOffset = Vector3.Zero;

        public Matrix OffsetMatrix { get; private set; }

        public Vector3 NaturalModelScaling { get { return Vector3.One / ScaleOffset; } }

        public List<Texture2D> Textures { get; set; }

        #region Private

        /// <summary>
        /// Gets or sets the World Matrix.
        /// </summary>
        private Matrix WorldM { get; set; }


        /// <summary>
        /// Gets or sets the Projection Matrix.
        /// </summary>
        private Matrix ProjM { get; set; }


        /// <summary>
        /// Gets or sets the View Matrix.
        /// </summary>
        private Matrix ViewM { get; set; }

        #endregion

        #endregion


        #region Init

        /// <summary>
        /// Creates a new Actor.
        /// </summary>
        /// <param name="Entity">The entity that this actor will represent.</param>
        public Actor3D_Model(Entity3D Entity, String ModelName) 
            : base(Entity)
        {
            Model = (Model)Game.GameRef.Content.Load<Model>(ModelName);
            ScaleOffset = Vector3.One;

            SkinningData Skin = Model.Tag as SkinningData;
            if (Skin != null) 
                SkinningData = Skin;

            Textures = new List<Texture2D>();
        }

        #endregion


        #region Draw

        /// <summary>
        /// Draws the entity onto the viewport.
        /// </summary>
        /// <param name="Viewport">The viewport onto which the entity is being rendered.</param>
        /// <param name="Time">Provides a snapshot of timing values.</param>
        public override void Draw(Viewport3D Viewport, GameTime Time) 
        {
            base.Draw(Viewport, Time);

            // Create world matrix.
            WorldM =
                OffsetMatrix *
                Matrix.CreateScale(Entity.Size) *
                Matrix.CreateRotationX(Entity.Orientation.X) *
                Matrix.CreateRotationY(Entity.Orientation.Y) *
                Matrix.CreateRotationZ(Entity.Orientation.Z) *
                Matrix.CreateTranslation(Entity.Position);

            // Update model animations
            Update(Time, WorldM);

            // If the model contains no animation information..
            if (SkinningData == null)
            // Just use the simple draw method.
            //Model.Draw(WorldM, Viewport.Camera.ViewMatrix, Viewport.Camera.ProjectionMatrix);
            {
                Matrix[] Transforms = new Matrix[Model.Bones.Count];
                Model.CopyAbsoluteBoneTransformsTo(Transforms);
                Int32 i = 0;
                foreach (ModelMesh Mesh in Model.Meshes)
                {
                    foreach (Effect Effect in Mesh.Effects)
                    {
                        Matrix WorldMatrix = Transforms[Mesh.ParentBone.Index] * WorldM;
                        Effect.CurrentTechnique = Effect.Techniques["Textured"];
                        Effect.Parameters["xView"].SetValue(Viewport.Camera.ViewMatrix);
                        Effect.Parameters["xProjection"].SetValue(Viewport.Camera.ProjectionMatrix);
                        Effect.Parameters["xTexture"].SetValue(Textures[i]);
                        Effect.Parameters["xWorld"].SetValue(WorldMatrix);
                        Effect.Parameters["xLightDirection"].SetValue(new Vector3(
                            Viewport.Camera.LightDirection.X, 
                            Viewport.Camera.LightDirection.Y, 
                           -Viewport.Camera.LightDirection.Z));
                        Effect.Parameters["xAmbient"].SetValue(Viewport.Camera.AmbientLight);
                        Effect.Parameters["xEnableLighting"].SetValue(true);
                        i = (i + 1) % Textures.Count;
                    }
                    Mesh.Draw();
                }
            }

            // Otherwise draw the skinned model.
            else
            {
                if (CurrentClip == null)
                    Model.Draw(WorldM, Viewport.Camera.ViewMatrix, Viewport.Camera.ProjectionMatrix);
                else
                {
                    foreach (ModelMesh Mesh in Model.Meshes)
                    {
                        foreach (SkinnedEffect Effect in Mesh.Effects)
                        {
                            Effect.SetBoneTransforms(SkinTransforms);

                            Effect.View = Viewport.Camera.ViewMatrix;
                            Effect.Projection = Viewport.Camera.ProjectionMatrix;

                            Effect.EnableDefaultLighting();

                            Effect.SpecularColor = new Vector3(1f);
                            Effect.SpecularPower = 16;
                        }

                        Mesh.Draw();
                    }
                }
            }
        }

        #endregion


        private void ComputeOffset()
        {
            OffsetMatrix =
                Matrix.CreateScale(ScaleOffset) *
                Matrix.CreateRotationX(OrientationOffset.X) *
                Matrix.CreateRotationY(OrientationOffset.Y) *
                Matrix.CreateRotationZ(OrientationOffset.Z) *
                Matrix.CreateTranslation(PositionOffset);
                
        }


        #region ANIMATION


        #region Fields


        // Information about the currently playing animation clip.
        public AnimationClip CurrentClip { get; protected set; }
        public Int32 CurrentTime { get; protected set; }
        public Int32 CurrentKeyFrame { get; protected set; }


        // Current animation transform matrices.
        public Matrix[] BoneTransforms { get; protected set; }
        public Matrix[] WorldTransforms { get; protected set; }
        public Matrix[] SkinTransforms { get; protected set; }


        // Backlink to the bind pose and skeleton hierarchy data.
        public SkinningData SkinningData
        {
            get { return _SkinningData; }
            set
            {
                _SkinningData = value;

                BoneTransforms = new Matrix[value.BindPose.Count];
                WorldTransforms = new Matrix[value.BindPose.Count];
                SkinTransforms = new Matrix[value.BindPose.Count];
            }
        }
        private SkinningData _SkinningData = null;


        #endregion

        /// <summary>
        /// Starts decoding the specified animation clip.
        /// </summary>
        public Boolean StartClip(String ClipName)
        {
            AnimationClip Clip = SkinningData.AnimationClips[ClipName];
            if (Clip == null) return false;

            CurrentClip = Clip;
            CurrentTime = 0;
            CurrentKeyFrame = 0;

            // Initialize bone transforms to the bind pose.
            SkinningData.BindPose.CopyTo(BoneTransforms, 0);

            return true;
        }


        /// <summary>
        /// Advances the current animation position.
        /// </summary>
        public void Update(GameTime Time, Matrix RootTransform)
        {
            if (SkinningData == null || CurrentClip == null) return;

            UpdateBoneTransforms(Time);
            UpdateWorldTransforms(RootTransform);
            UpdateSkinTransforms();
        }


        /// <summary>
        /// Helper used by the Update method to refresh the BoneTransforms data.
        /// </summary>
        protected void UpdateBoneTransforms(GameTime Time)
        {
            // Update the animation position.
            CurrentTime += Time.ElapsedGameTime.Milliseconds;

            // If we reached the end, loop back to the start.
            while (CurrentTime >= CurrentClip.Duration)
                CurrentTime -= CurrentClip.Duration;

            // Read keyframe matrices.
            IList<KeyFrame> KeyFrames = CurrentClip.KeyFrames;

            for (; CurrentKeyFrame < KeyFrames.Count; CurrentKeyFrame++)
            {
                if (KeyFrames[CurrentKeyFrame].Time > CurrentTime)
                    return;

                BoneTransforms[KeyFrames[CurrentKeyFrame].Bone] = KeyFrames[CurrentKeyFrame].Transform;
            }
            CurrentKeyFrame = 0;
        }


        /// <summary>
        /// Helper used by the Update method to refresh the WorldTransforms data.
        /// </summary>
        protected void UpdateWorldTransforms(Matrix RootTransform)
        {
            // Root bone.
            WorldTransforms[0] = BoneTransforms[0] * RootTransform;

            // Child bones.
            for (Int32 Bone = 1; Bone < WorldTransforms.Length; Bone++)
            {
                Int32 ParentBone = SkinningData.SkeletonHierarchy[Bone];

                WorldTransforms[Bone] = BoneTransforms[Bone] *
                                             WorldTransforms[ParentBone];
            }
        }


        /// <summary>
        /// Helper used by the Update method to refresh the SkinTransforms data.
        /// </summary>
        public void UpdateSkinTransforms()
        {
            for (int Bone = 0; Bone < SkinTransforms.Length; Bone++)
            {
                SkinTransforms[Bone] = SkinningData.InverseBindPose[Bone] *
                                            WorldTransforms[Bone];
            }
        }


        #endregion
    }
}